{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "43c96ba4",
   "metadata": {},
   "source": [
    "# 폴백(fallback)\n",
    "\n",
    "LLM 애플리케이션에는 LLM API 문제, 모델 출력 품질 저하, 다른 통합 관련 이슈 등 다양한 오류/실패가 존재합니다. 이러한 문제를 우아하게 처리하고 격리하는데 `fallback` 기능을 활용할 수 있습니다.\n",
    "\n",
    "중요한 점은 fallback 을 LLM 수준뿐만 아니라 전체 실행 가능한 수준에 적용할 수 있다는 것입니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d66ad995",
   "metadata": {},
   "source": [
    "## LLM API Error 에 대처 방법\n",
    "\n",
    "LLM API 오류 처리는 `fallback` 을 사용하는 가장 일반적인 사례 중 하나입니다.\n",
    "\n",
    "LLM API 에 대한 요청은 다양한 이유로 실패할 수 있습니다. API가 다운되었거나, 속도 제한에 도달했거나, 그 외 여러 가지 문제가 발생할 수 있습니다. 따라서 `fallback` 을 사용하면 이러한 유형의 문제로부터 보호하는 데 도움이 될 수 있습니다.\n",
    "\n",
    "**중요**: 기본적으로 많은 LLM 래퍼(wrapper)는 오류를 포착하고 재시도합니다. `fallback` 을 사용할 때는 이러한 기본 동작을 해제하는 것이 좋습니다. 그렇지 않으면 첫 번째 래퍼가 계속 재시도하고 실패하지 않을 것입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64ca48ba",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install -qU langchain langchain-openai"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0feba5a7",
   "metadata": {},
   "source": [
    "먼저, OpenAI에서 `RateLimitError` 가 발생하는 경우에 대해 모의 테스트를 해보겠습니다. `RateLimitError` 는 OpenAI API의 **API 호출 비용 제한을 초과했을 때 발생하는 오류** 입니다.\n",
    "\n",
    "이 오류가 발생하면 일정 시간 동안 API 요청이 제한되므로, 애플리케이션에서는 이에 대한 적절한 처리가 필요합니다. 모의 테스트를 통해 `RateLimitError` 발생 시 애플리케이션이 어떻게 동작하는지 확인하고, 오류 처리 로직을 점검할 수 있습니다.\n",
    "\n",
    "이를 통해 실제 운영 환경에서 발생할 수 있는 문제를 사전에 방지하고, 안정적인 서비스를 제공할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "b2190f3a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_anthropic import ChatAnthropic\n",
    "from langchain_openai import ChatOpenAI"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "99dbb4ba",
   "metadata": {},
   "outputs": [],
   "source": [
    "from unittest.mock import patch\n",
    "\n",
    "import httpx\n",
    "from openai import RateLimitError\n",
    "\n",
    "request = httpx.Request(\"GET\", \"/\")  # GET 요청을 생성합니다.\n",
    "response = httpx.Response(\n",
    "    200, request=request\n",
    ")  # 200 상태 코드와 함께 응답을 생성합니다.\n",
    "# \"rate limit\" 메시지와 응답 및 빈 본문을 포함하는 RateLimitError를 생성합니다.\n",
    "error = RateLimitError(\"rate limit\", response=response, body=\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18d67bd7",
   "metadata": {},
   "source": [
    "`openai_llm` 변수에 `ChatOpenAI` 객체를 생성하고, `max_retries` 매개변수를 0으로 설정하여 **API 호출비용 제한 등으로 인한 재시도를 방지** 합니다.\n",
    "\n",
    "`with_fallbacks` 메서드를 사용하여 `anthropic_llm`을 `fallback` LLM으로 설정하고, 이를 `llm` 변수에 할당합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "482ae9d6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# OpenAI의 ChatOpenAI 모델을 사용하여 openai_llm 객체를 생성합니다.\n",
    "# max_retries를 0으로 설정하여 속도 제한 등으로 인한 재시도를 방지합니다.\n",
    "openai_llm = ChatOpenAI(max_retries=0)\n",
    "\n",
    "# Anthropic의 ChatAnthropic 모델을 사용하여 anthropic_llm 객체를 생성합니다.\n",
    "anthropic_llm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n",
    "\n",
    "# openai_llm을 기본으로 사용하고, 실패 시 anthropic_llm을 대체로 사용하도록 설정합니다.\n",
    "llm = openai_llm.with_fallbacks([anthropic_llm])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "1681e261",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "에러 발생\n"
     ]
    }
   ],
   "source": [
    "# OpenAI LLM을 먼저 사용하여 오류가 발생하는 것을 보여줍니다.\n",
    "with patch(\"openai.resources.chat.completions.Completions.create\", side_effect=error):\n",
    "    try:\n",
    "        # \"닭이 길을 건넌 이유는 무엇일까요?\"라는 질문을 OpenAI LLM에 전달합니다.\n",
    "        print(openai_llm.invoke(\"Why did the chicken cross the road?\"))\n",
    "    except RateLimitError:\n",
    "        # 오류가 발생하면 오류를 출력합니다.\n",
    "        print(\"에러 발생\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "68d4e1d4",
   "metadata": {},
   "source": [
    "OpenAI API의 비용 제한(rate limit)을 시뮬레이션하고, API 호출비용 제한 오류가 발생했을 때의 동작을 테스트하는 예제입니다.\n",
    "\n",
    "OpenAI 의 GPT 모델을 시도하는데 에러가 발생했고, fallback 모델인 `Anthropic` 의 모델이 대신 추론을 수행했다는 점을 확인할 수 있습니다.\n",
    "\n",
    "`with_fallbacks()` 로 대체 모델이 설정되어 있고, 대체 모델이 성공적으로 수행했다면, `RateLimitError` 가 발생하지 않습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "9e196ac4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "content='대한민국의 수도는 서울특별시입니다. \\n\\n서울은 한반도 중앙에 위치하며, 한강을 끼고 있는 대한민국 최대의 도시입니다. 서울의 인구는 약 1000만 명으로 전체 한국 인구의 약 20%가 서울에 거주하고 있습니다. \\n\\n서울은 조선시대부터 한국의 수도 역할을 해왔으며, 현재는 정치, 경제, 사회, 문화 등 대한민국의 중심지 역할을 하고 있습니다. 대한민국 정부 주요 기관들이 서울에 위치해 있으며, 다양한 기업의 본사도 서울에 많이 자리잡고 있습니다.\\n\\n또한 고궁, 박물관, 현대적 건축물 등 새로운 것과 전통적인 것이 조화를 이루는 매력적인 도시로서, 많은 관광객이 방문하는 글로벌 도시이기도 합니다.' response_metadata={'id': 'msg_012yS3DPqGNPoAoyVQR2xrWE', 'content': [ContentBlock(text='대한민국의 수도는 서울특별시입니다. \\n\\n서울은 한반도 중앙에 위치하며, 한강을 끼고 있는 대한민국 최대의 도시입니다. 서울의 인구는 약 1000만 명으로 전체 한국 인구의 약 20%가 서울에 거주하고 있습니다. \\n\\n서울은 조선시대부터 한국의 수도 역할을 해왔으며, 현재는 정치, 경제, 사회, 문화 등 대한민국의 중심지 역할을 하고 있습니다. 대한민국 정부 주요 기관들이 서울에 위치해 있으며, 다양한 기업의 본사도 서울에 많이 자리잡고 있습니다.\\n\\n또한 고궁, 박물관, 현대적 건축물 등 새로운 것과 전통적인 것이 조화를 이루는 매력적인 도시로서, 많은 관광객이 방문하는 글로벌 도시이기도 합니다.', type='text')], 'model': 'claude-3-opus-20240229', 'role': 'assistant', 'stop_reason': 'end_turn', 'stop_sequence': None, 'type': 'message', 'usage': Usage(input_tokens=22, output_tokens=339)}\n"
     ]
    }
   ],
   "source": [
    "# OpenAI API 호출 시 에러가 발생하는 경우 Anthropic 으로 대체하는 코드\n",
    "with patch(\"openai.resources.chat.completions.Completions.create\", side_effect=error):\n",
    "    try:\n",
    "        # \"대한민국의 수도는 어디야?\"라는 질문을 언어 모델에 전달하여 응답을 출력합니다.\n",
    "        print(llm.invoke(\"대한민국의 수도는 어디야?\"))\n",
    "    except RateLimitError:\n",
    "        # RateLimitError가 발생하면 \"에러 발생\"를 출력합니다.\n",
    "        print(\"에러 발생\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "866996f2",
   "metadata": {},
   "source": [
    "`llm.with_fallbacks()` 설정한 모델도 일반 runnable 모델과 동일하게 동작합니다.\n",
    "\n",
    "아래의 코드 역시 \"오류 발생\"은 출려되지 않습니다. fallbacks 모델이 잘 수행했기 때문입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "b5558e96",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "content='대한민국의 수도는 서울특별시입니다.' response_metadata={'id': 'msg_013FiBouyK7dRti21HMLRvwR', 'content': [ContentBlock(text='대한민국의 수도는 서울특별시입니다.', type='text')], 'model': 'claude-3-opus-20240229', 'role': 'assistant', 'stop_reason': 'end_turn', 'stop_sequence': None, 'type': 'message', 'usage': Usage(input_tokens=46, output_tokens=23)}\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"질문에 짧고 간결하게 답변해 주세요.\",  # 시스템 역할 설명\n",
    "        ),\n",
    "        (\"human\", \"{country} 의 수도는 어디입니까?\"),  # 사용자 질문 템플릿\n",
    "    ]\n",
    ")\n",
    "chain = prompt | llm  # 프롬프트와 언어 모델을 연결하여 체인 생성\n",
    "# chain = prompt | ChatOpenAI() # 이 코드이 주석을 풀고 실행하면 \"오류 발생\" 문구가 출력됩니다.\n",
    "with patch(\"openai.resources.chat.completions.Completions.create\", side_effect=error):\n",
    "    try:\n",
    "        print(chain.invoke({\"country\": \"대한민국\"}))  # 체인을 호출하여 결과 출력\n",
    "    except RateLimitError:  # API 비용 제한 오류 처리\n",
    "        print(\"오류 발생\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58a91a9b",
   "metadata": {},
   "source": [
    "## 처리해야할 오류를 구체적으로 명시한 경우\n",
    "\n",
    "특정 오류를 처리하기 위해 `fallback` 이 호출되는 시점을 더 명확하게 지정할 수도 있습니다. 이를 통해 `fallback` 메커니즘이 동작하는 상황을 보다 세밀하게 제어할 수 있습니다.\n",
    "\n",
    "예를 들어, 특정 예외 클래스나 오류 코드를 지정함으로써 해당 오류가 발생했을 때만 fallback 로직이 실행되도록 설정할 수 있습니다. 이렇게 하면 **불필요한 `fallback` 호출을 줄이고, 오류 처리의 효율성을 높일 수** 있습니다.\n",
    "\n",
    "아래의 예제에서는 \"오류 발생\" 문구가 출력됩니다. 이유는 `exceptions_to_handle` 파라미터에서 `KeyboardInterrupt` 예외가 발생시에만 `fallback` 이 구동되도록 설정했기 때문입니다. 따라서, `KeyboardInterrupt` 를 제외한 모든 예외에서는 `fallback` 이 발생하지 않습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "d6e33645",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "content='대한민국의 수도는 서울특별시입니다.' response_metadata={'id': 'msg_01UbBNaKkSPecHATCKVwyMHQ', 'content': [ContentBlock(text='대한민국의 수도는 서울특별시입니다.', type='text')], 'model': 'claude-3-opus-20240229', 'role': 'assistant', 'stop_reason': 'end_turn', 'stop_sequence': None, 'type': 'message', 'usage': Usage(input_tokens=46, output_tokens=23)}\n"
     ]
    }
   ],
   "source": [
    "llm = openai_llm.with_fallbacks(\n",
    "    # 대체 LLM으로 anthropic_llm을 사용하고, 예외 처리할 대상으로 KeyboardInterrupt를 지정합니다.\n",
    "    [anthropic_llm],\n",
    "    exceptions_to_handle=(KeyboardInterrupt,),  # 예외 처리 대상을 지정합니다.\n",
    ")\n",
    "\n",
    "# 프롬프트와 LLM을 연결하여 체인을 생성합니다.\n",
    "chain = prompt | llm\n",
    "with patch(\"openai.resources.chat.completions.Completions.create\", side_effect=error):\n",
    "    try:\n",
    "        # 체인을 호출하여 결과를 출력합니다.\n",
    "        print(chain.invoke({\"country\": \"대한민국\"}))\n",
    "    except RateLimitError:\n",
    "        # RateLimitError 예외가 발생하면 \"오류 발생\"를 출력합니다.\n",
    "        print(\"오류 발생\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2eafd75",
   "metadata": {},
   "source": [
    "## fallback 에 여러 모델을 순차적으로 지정\n",
    "\n",
    "`fallback` 모델에 1가지 모델만 지정할 수 있는 것은 아니고, 여러 개의 모델을 지정 가능합니다. 이렇게 여러개의 모델을 지정했을 때 순차적으로 시도하게 됩니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "0e68510e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.prompts import PromptTemplate\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# 프롬프트 생성\n",
    "prompt_template = (\n",
    "    \"질문에 짧고 간결하게 답변해 주세요.\\n\\nQuestion:\\n{question}\\n\\nAnswer:\"\n",
    ")\n",
    "prompt = PromptTemplate.from_template(prompt_template)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6185c984",
   "metadata": {},
   "source": [
    "오류를 발생하는 chain 과 정상적인 chain 2가지를 생성합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "id": "e3a2416f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 여기서는 쉽게 오류를 발생시킬 수 있는 잘못된 모델 이름을 사용하여 체인을 생성할 것입니다.\n",
    "chat_model = ChatOpenAI(model_name=\"gpt-fake\")\n",
    "bad_chain = prompt | chat_model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "id": "2eec12f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# fallback 체인을 생성합니다.\n",
    "fallback_chain1 = prompt | ChatOpenAI(model=\"gpt-3.6-turbo\") # 오류\n",
    "fallback_chain2 = prompt | ChatOpenAI(model=\"gpt-3.5-turbo\") # 정상\n",
    "fallback_chain3 = prompt | ChatOpenAI(model=\"gpt-4-turbo-preview\") # 정상"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "id": "5f7781b4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='서울입니다.', response_metadata={'token_usage': {'completion_tokens': 5, 'prompt_tokens': 46, 'total_tokens': 51}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_b28b39ffa8', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 59,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 두 개의 체인을 결합하여 최종 체인을 생성합니다.\n",
    "chain = bad_chain.with_fallbacks(\n",
    "    [fallback_chain1, fallback_chain2, fallback_chain3])\n",
    "# 생성된 체인을 호출하여 입력값을 전달합니다.\n",
    "chain.invoke({\"question\": \"대한민국의 수도는 어디야?\"})"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
